import json
import os
import time
from json import JSONDecodeError

import requests
from robot.api import logger
from robotide.action import ActionInfo
from robotide.pluginapi import Plugin, RideLogMessage
import wx

BASE_URL = 'BASE_URL'

class SwaggerPlugin(Plugin):
    """根据Swagger地址自动生成测试用例插件"""

    def __init__(self, application):
        Plugin.__init__(self, application, name=None, doc=None, metadata=None,
                        default_settings=None, initially_enabled=True)

        self._app = application

        self._menu = [
            ActionInfo('Tools', '根据Swagger生成测试用例', self.swagger_to_case, position=95),
        ]
        self._swagger_json = {}
        self._variables = {}
        self._swagger_title = ""

    def enable(self):
        self.unregister_actions()
        self.register_actions(self._menu)

    def disable(self):
        self.unregister_actions()

    def swagger_to_case(self, even):
        dlg = wx.TextEntryDialog(None, '输入swagger的地址,应该是以/v2/api-docs结尾', 'Swagger')
        dlg.SetValue("http://localhost:80/v2/api-docs")  # http://172.16.17.122:52180/v2/api-docs
        choice = dlg.ShowModal()
        swagger_url = dlg.GetValue()
        dlg.Destroy()
        if choice != wx.ID_OK:
            return
        # 获取Swagger
        swagger_file_path = self._save_swagger_file(swagger_url)
        if not swagger_file_path:
            return
        # 加载swagger转成json
        if not self._load_swagger_file(swagger_file_path):
            return
        try:
            # 解析swagger，转成保存HttpRun的测试用例Json
            test_case_json_file = os.path.join(self._get_current_working_dir(), "Case", "Resources", "Swagger",
                                               self._swagger_title)
            results = self._gen_testcase_json(test_case_json_file)
            # 生成robot测试用例
            case_count = self._gen_testcase(results, swagger_url)
            if case_count:
                wx.MessageBox(f"自动生成{case_count}个测试用例在{self._swagger_title}目录下\n\nRide将重新加载用例！", "SwaggerPlugin",
                              wx.OK | wx.ICON_INFORMATION)
                # 刷新界面重新加载用例
                path = self._app._initial_path or self._app._get_latest_path()
                self.open_suite(path)
        except Exception as e:
            self.error(str(e))

    def log(self, message):
        mess = RideLogMessage(message=message)
        mess.publish()

    def error(self, message):
        self.log(message)
        wx.MessageBox(message, "SwaggerPlugin", wx.OK | wx.ICON_ERROR)

    def _gen_testcase(self, cases, swagger_url):
        suit_header = r"""*** Settings ***
Library    Lib
Library    Collections 

*** Variables ***
${SWAGGER_BASE_URL}    \    # 接口测试的主机地址，如果为空，则使用Swagger文件中定义的Host地址
&{SWAGGER_COOKIES}    # 设置消息要带的Cookies，键值对方式，如 ssid=12344
&{SWAGGER_HEADERS}    #设置HTTP消息的公共Header头，键值对方式，如User-Agent=ride   

**** Test Case ***
"""
        case_template = r"""{name}
    [Documentation]    本用例根据 {swagger_url} Swagger自动生成, 数据文件: {json_path}
    ...
    ...    URL : *{url}*   {description}
    ...
    ...    *请求参数* :
    ...    ``{params}``
    ...
    ...    *请求Body* :
    ...    ``{body}``
    [Tags]    {tags}
    ${{params}}=    Create Dictionary
    Run Keyword IF    '${{SWAGGER_BASE_URL}}' != ''    Set To Dictionary    ${{params}}    BASE_URL    ${{SWAGGER_BASE_URL}}
    ${{update_body}}=    Create Dictionary
    # 发送接口消息
    ${{result}}=    ZHttpRunTest    ${{EXECDIR}}{json_path_expr}    ${{params}}    ${{update_body}}    ${{SWAGGER_HEADERS}}    ${{SWAGGER_COOKIES}}
    #${{result}} 为接口返回的消息内容，你可以再做进一步检验
    #获取HTTP消息响应码：${{_HTTP_RESPONSE.status_code}}，获取响应消息的Header头中的Connection值：${{_HTTP_RESPONSE.headers['Connection']}}
"""

        suit_path = os.path.join(self._get_current_working_dir(), "Case", f"swagger_{self._delete_special_char(self._swagger_title)}")
        if not os.path.exists(suit_path):
            os.makedirs(suit_path)

        case_count = 0
        is_re_write_test_case = 0  # 没有设置， 1 重写 2 不重写
        new_suit_file_list = []
        for case in cases:
            case_json = case["case"]
            json_path = case["path"]
            controller = case_json["config"].get("tag", "Default")
            suit_file = os.path.join(suit_path, f"{self._delete_special_char(controller)}.robot")

            if os.path.exists(suit_file):
                if controller not in new_suit_file_list:
                    if is_re_write_test_case == 0:
                        # 如果文件存在，并且不是这次新建的，并且没有询问过用户
                        choice = wx.MessageBox(f"{controller} 用例文件已经存在，确定重新生成覆盖你之前的用例吗？\n 注意：如果还有文件存在，都执行相同操作！",
                                               "SwaggerPlugin",
                                               wx.YES_NO | wx.ICON_QUESTION)

                        self.log(f"Your choice {choice}")
                        is_re_write_test_case = 1 if choice == wx.YES else 2
                        self.log(f"My choice {is_re_write_test_case}")

                    if is_re_write_test_case == 2:
                        # 如果询问过用户不重写了，则跳出
                        continue
                    #  要重写，则重新写suit头
                    with open(suit_file, "w", encoding="utf-8") as f:
                        f.write(suit_header)
                        self.log(f"write header {suit_file}")
                    #  加到本次新建的文件列表中
                    new_suit_file_list.append(controller)
                # else: 如果文件存在，但是本次新建的，则直接到后面写用例
            else:
                # 如果文件不存在 则写suit头，并且加到这次新建的文件列里
                with open(suit_file, "w", encoding="utf-8") as f:
                    f.write(suit_header)
                    self.log(f"write header {suit_file}")
                new_suit_file_list.append(controller)

            # 开始追加写用例
            with open(suit_file, "a", encoding="utf-8") as f:
                self.log(f"Write case {case_json['config']['name']}")
                if json_path.startswith(self._get_current_working_dir()):
                    header_len = len(self._get_current_working_dir())
                    json_path = json_path[header_len:]

                json_path_expr = json_path.replace("/", "${/}").replace("\\", "${/}")
                url = case_json["teststeps"][0]["request"]["url"]
                request_param = case_json["config"].get("variables")
                request_param.update(case_json["teststeps"][0]["request"].get("params", {}))
                request_body = case_json["teststeps"][0]["request"].get("json")
                if not request_body:
                    request_body = case_json["teststeps"][0]["request"].get("data")
                tags = "    ".join([case_json["teststeps"][0]["request"].get("method"),
                                    case_json["config"].get("tag")])

                data = {"name": case_json["config"]["name"],
                        "description": case_json["config"]["description"],
                        "json_path": json_path,
                        "json_path_expr": json_path_expr,
                        "url": url,
                        "params": request_param,
                        "body": request_body,
                        "swagger_url": swagger_url,
                        "tags": tags}
                f.write(case_template.format(**data))
            case_count += 1
        return case_count

    def _delete_special_char(self, path_string):
        special_char = '/\:*"<>|?'
        for char in special_char:
            path_string = path_string.replace(char, '')
        return path_string

    def _save_swagger_file(self, url):
        if url.endswith("swagger-ui.html"):
            url = url[:-15] + "v2/api-docs"

        try:
            self.log(f"Trying to get swagger info: {url}")
            r = requests.get(url).json()
            # version = r["swagger"]
            # if version != "2.0":
            #     self.error(f"当前只支持Swager 2.0的版本！而你的swagger版本为{version}")
            #     return None
            title = r["info"].get("title")
            if not title:
                title = r["host"]
                title = title.replace(".", "_").replace(":", "_")
            self._swagger_title = title  # 保存，后续生成用例json会用到

            swagger_path = os.path.join(self._get_current_working_dir(), "Case", "Resources", "Swagger")
            if not os.path.exists(swagger_path):
                os.makedirs(swagger_path)

            swagger_path = os.path.join(swagger_path, f"{title}.json")
            self.log(f"Trying to save swagger file in {swagger_path}")

            with open(swagger_path, 'w', encoding='utf-8') as f:
                json.dump(r, f, ensure_ascii=False, indent=4)

            return swagger_path
        except Exception as e:
            self.error(f"请求swagger({url})失败: {e}")
            return None

    def _get_current_working_dir(self):
        if not os.path.isdir(self.model.suite.source):
            return os.path.dirname(self.model.suite.source)
        return self.model.suite.source

    def _load_swagger_file(self, swagger_file_path):
        self.log(f"Trying to load swagger file {swagger_file_path}")
        # 加载swagger文件
        with open(swagger_file_path, mode="rb") as f:
            try:
                self._swagger_json = json.load(f)
            except (TypeError, JSONDecodeError) as ex:
                self.error(f"failed to load swagger file {swagger_file_path}: {ex}")
                return False
        return True

    def _gen_testcase_json(self, save_path, filter_url=None, exclude_url=None):
        """生成测试用例Json文件
        返回
        """
        # 添加host变量
        self._make_base_url()

        results = []
        paths = self._swagger_json['paths']
        for url, path_data in paths.items():
            if filter_url and filter_url not in url:
                continue
            if exclude_url and exclude_url in url:
                continue

            for method, method_data in path_data.items():
                deprecated = method_data.get("deprecated", False)
                if deprecated:
                    continue
                try:
                    case_tag, case_id = self._get_testcase_tag_and_id(method_data, url, method)
                    test_case = self._make_testcase(url, method, method_data, case_id)
                    json_path = self._save_testcase(test_case, save_path, case_tag, case_id)
                    result = {"path": json_path, "case": test_case}
                    results.append(result)
                except Exception as e:
                    self.error(str(e))

        return results

    def _save_testcase(self, test_case, save_path, tag, case_id):
        path = os.path.join(save_path, tag)
        if not os.path.exists(path):
            os.makedirs(path)
        # 保存 分类，写robot的用例时需要用到
        test_case["config"]["tag"] = tag

        # 如果之前有，直接覆盖
        json_file = os.path.join(path, f"{case_id}.json")
        try:
            with open(json_file, "w", encoding="utf-8") as outfile:
                my_json_str = json.dumps(test_case, ensure_ascii=False, indent=4)
                if isinstance(my_json_str, bytes):
                    my_json_str = my_json_str.decode("utf-8")
                outfile.write(my_json_str)
            self.log(f"Generate JSON testcase successfully: {json_file}")
        except Exception as e:
            self.log(str(e))

        return json_file

    def _make_base_url(self):
        # 获得host地址, 没测试过有basePath的情况
        host = self._swagger_json.get("host") or "127.0.1"
        base_path = self._swagger_json.get("basePath") or "/"
        if base_path == '/':
            url_root = f"http://{host}"
        else:
            url_root = f"http://{host}{base_path}"
        self._variables["BASE_URL"] = url_root

    def _get_testcase_tag_and_id(self, method_data, url, method):
        # case_tag 首先找summary, 没有就通过url
        # case_id 首先找operationId,这个是唯一的，没有就tag+method
        tags = method_data.get("tags")
        if tags and len(tags) > 0:
            case_tag = tags[0]
        else:
            summary = method_data.get("summary")
            if summary:
                case_tag = summary
            else:
                case_tag = url.replace("/", "_")
        case_id = method_data.get("operationId") or case_tag + method
        return case_tag, case_id

    def _make_testcase(self, url, method, method_data, case_id):
        """ Extract info from swagger file and prepare for testcase
        """
        config = self._prepare_config(case_id, method_data, method)
        # 每个用例就一个步骤
        teststep = self._prepare_teststep(url, method, method_data)

        testcase = {"config": config, "teststeps": [teststep]}
        return testcase

    def _prepare_config(self, case_id, method_data, method):
        config_dict = {"id": case_id, "variables": {}, "verify": False}
        summary = method_data.get("summary")
        if summary:
            summary = summary.replace(" ", "_")
            config_dict["name"] = f"{summary}_{method}"
        else:
            config_dict["name"] = case_id
        config_dict["description"] = method_data.get("description", "")  # 描述默认为空，后面要写到用例的Document中

        variables = self._variables.copy()
        # 把path变量找出来，放到config中
        parameters = method_data.get('parameters', [])
        for param in parameters:
            location = param.get('in')
            if location == 'path':
                var = dict()
                var[param.get("name")] = self.__get_params_example_data(param)
                variables.update(var)
        config_dict["variables"] = variables
        return config_dict

    def _prepare_teststep(self, url, method, method_data):
        teststep_dict = {"name": method, "request": {"headers": {}, "params": {}}, "validate": []}

        # url 参数化
        new_url = url.replace('{', '$').replace('}', '')
        teststep_dict["request"]["url"] = f'${BASE_URL}{new_url}'
        # method
        teststep_dict["request"]["method"] = method.upper()

        # header
        content_types = method_data.get("consumes")
        if content_types:
            teststep_dict["request"]["headers"]["Content-Type"] = ";".join(content_types)
        accept_types = method_data.get("produces")
        if accept_types:
            teststep_dict["request"]["headers"]["Accept"] = ";".join(accept_types)
        # params
        parameters = method_data.get('parameters', [])
        for param in parameters:
            location = param.get('in')
            if location == 'query':
                param_name = param.get("name")
                teststep_dict["request"]["params"].update({param_name: self.__get_params_example_data(param)})
            elif location == "body":
                teststep_dict["request"]["json"] = self.__get_params_example_data(param)
            elif location == "formData":
                teststep_dict["request"]["data"] = self.__get_params_example_data(param)
            elif location == "header":
                param_name = param.get("name")
                teststep_dict["request"]["headers"][param_name] = self.__get_params_example_data(param)

        return teststep_dict

    def __get_params_example_data(self, params: dict, param_names: list = []):
        """递归获取到参数示例值
        param_names是为了防止循环引用
        """
        example = params.get("example")

        if example:
            return example

        ref = params.get("$ref")
        if ref:
            if not ref.startswith(r"#/"):
                self.log(f"Not Support ref Object {ref}")
                return None
            parts = ref[2:].split(r"/")
            document = self._swagger_json
            for part in parts:
                try:
                    document = document[part]
                except (TypeError, LookupError) as e:
                    self.log(f"Unresolvable JSON ref pointer: {ref} {e}")
                    return None
            # 增加循环引用判断
            if part in param_names:
                return None
            param_names.append(part)
            return self.__get_params_example_data(document, param_names)

        schema = params.get("schema")
        if schema:
            return self.__get_params_example_data(schema, param_names)

        param_type = params.get("type")
        if param_type == "object":
            result = {}
            properties = params.get("properties", {})
            for name, value in properties.items():
                param_names.append(name)
                result[name] = self.__get_params_example_data(value, param_names)
            additional_properties = params.get("additionalProperties")
            if additional_properties:
                param_names.append("additionalProp1")
                result["additionalProp1"] = self.__get_params_example_data(additional_properties, param_names)
            return result
        elif param_type == "array":
            items = params.get("items")
            result = [self.__get_params_example_data(items, param_names)]
            return result
        elif param_type == "string":
            format = params.get("format")
            if format == "date":
                return time.strftime('%Y-%m-%d', time.gmtime())
            elif format == "date-time":
                # 2020-11-16T06:33:31.683Z
                return time.strftime('%Y-%m-%dT%H:%M:%S', time.gmtime())
            else:
                return "string"
        elif param_type == "integer":
            return 0
        elif param_type == "number":
            return 0.0
        elif param_type == "boolean":
            return True
        else:
            logger.error(f"Not Support Params Type {param_type}")
            return None

    def __get_object_dict(self, json_object):
        _type = json_object.get('type')




if __name__ == '__main__':
    plugin = SwaggerPlugin(None)
    plugin._save_swagger_file("http://10.45.50.133:7000/oss/v2/api-docs")
